package gsw.tool.arithmetic;

import java.util.ArrayList;
import java.util.List;

import gsw.tool.common.Tool;
import gsw.tool.common.ToolShow;

/**
 * 动态规划
 *
 * @author Asus
 */
public class DynamicProgram {
    // 代表无穷大
    final static int M = 0xFFFFFF;

    public static void test() {
        //测试LCS算法
        String result = getLCS("ABCBDAB", "BDCABA");
        ToolShow.log(result);

        //测试KMP算法
        String str = "abdcefghzabdcefghijklmabdcefghijklmncffdf";
        String dest = "abdcefghijklmn";
        int i = KMP(str, dest);
        ToolShow.log(dest + "在" + str + "中匹配的起点为：" + i);

        //测试floyd算法
        int[][] matrix = new int[5][5];
        matrix[0] = new int[]{0, 3, M, 8, M};
        matrix[1] = new int[]{3, 0, 9, 7, M};
        matrix[2] = new int[]{M, 9, 0, 10, 11};
        matrix[3] = new int[]{8, 7, 10, 0, 17};
        matrix[4] = new int[]{M, M, 11, 17, 0};
        List<String> pathList = floyd(matrix);
        ToolShow.printArr(matrix);
        ToolShow.log(pathList.toString());
    }

    /**
     * 斐波那契数列
     * <p>
     * 在讲解递归时，我们使用递归实现了斐波那契数列。现在使用动态规划的思想来实现斐波那契数列。
     */
    public static double fibonacciSequence(int n) {
        double[] array = new double[n];
        //当n=0或1时，直接返回1
        array[0] = 1;
        array[1] = 1;
        //当n>1时，返回前两个数的和
        for (int i = 2; i < array.length; i++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        return array[n - 1];
    }

    /**
     * LCS算法
     * <p>
     * 一个序列A任意删除若干个字符得到新序列B，则A叫做B的子序列。
     * LCS是Longest Common Subsequence的缩写，即最长公共子序列。两个序列X和Y的公共子序列中,长度最长的那个,定义为X和Y的最长公共子序列。
     * LCS的操作步骤：
     * 1. 声明序列X和Y，声明一个二维数组 f[i][j] 表示 X 的 i 位和 Y 的 j 位以前的最长公共子序列的长度，声明一个函数same(a,b)当 X 的第 a 位与 Y 的第 b 位完全相同时为“1”，否则为“0”；
     * 2. 填充二维数组 f[i][j] 。当i=0且j=0，f[0][0] = same(0,0)；否则，f[i][j] = max{f[i-1][j-1] + same(i,j),f[i-1][j],f[i][j-1]}；
     * 3. 回溯数组。声明一个序列Z为X和Y的最长公共子序列，比较X[i]与Y[j]：
     * - 如果相等，说明他们属于Z，将他们加入Z，然后i--,j--,继续比较X[i]与Y[j]；
     * - 如果不相等，并且f[i][j - 1] > f[i - 1][j],说明X[j]不属于Z，然后j--,继续比较X[i]与Y[j]；
     * - 如果不相等，并且f[i][j - 1] < f[i - 1][j],说明X[i]不属于Z，然后i--,继续比较X[i]与Y[j]；
     * - 如果不相等，并且f[i][j - 1] = f[i - 1][j],说明X[i]不属于Z或X[j]不属于Z，然后i--或者j--,继续比较X[i]与Y[j]；
     * - 当i=0并且j=0时，比较完毕，将Z倒序，即为X和Y的最长公共子序列。
     * <p>
     * LCS的应用：相似度的比较(如亲子鉴定、图形相似比较)。
     *
     * @param x 序列X
     * @param y 序列Y
     * @return
     */
    public static String getLCS(String x, String y) {
        if (Tool.isEmpty(x) || Tool.isEmpty(x)) {
            return null;
        }
        char[] a = x.toCharArray();
        char[] b = y.toCharArray();
        int[][] arr = new int[a.length][b.length];
        // 使用动态规划的方式填入数据:相同取左上+1，不同去Max(左，上)；
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++) {
                //a[i]=a[j]则公共子序列的长度+1，否则+0
                int length = a[i] == b[j] ? 1 : 0;
                if (i == 0 && j == 0) {
                    arr[i][j] = length;
                } else {
                    //左边位置对应的最长公共子序列的长度
                    int left = i == 0 ? 0 : arr[i - 1][j];
                    //上边位置对应的最长公共子序列的长度
                    int top = j == 0 ? 0 : arr[i][j - 1];
                    //左上位置对应的最长公共子序列的长度
                    int lt = (i == 0 || j == 0) ? 0 : arr[i - 1][j - 1];
                    arr[i][j] = Math.max(left, top);
                    arr[i][j] = Math.max(lt + length, arr[i][j]);
                }
            }
        }

        //最长公共子序列
        StringBuffer sb = new StringBuffer();
        int i = a.length - 1;
        int j = b.length - 1;
        while (i >= 0 && j >= 0) {
            //左边位置对应的最长公共子序列的长度
            int left = i == 0 ? 0 : arr[i - 1][j];
            //上边位置对应的最长公共子序列的长度
            int top = j == 0 ? 0 : arr[i][j - 1];
            if (a[i] == b[j]) {
                sb.append(a[i]);
                i--;
                j--;
                //上边大就继续比较上边
            } else if (top > left) {
                j--;
                //左边大就继续比较左边
            } else {
                i--;
            }
        }
        return sb.reverse().toString();
    }

    /**
     * KMP算法是一种改进的字符串匹配算法，利用匹配失败后的信息，尽量减少模式串与主串的匹配次数以达到快速匹配的目的。而在KMP算法中，对于每一个模式串我们会事先计算出模式串的内部匹配信息，在匹配失败时最大的移动模式串，以减少匹配次数。
     * 分析：
     * 1. 声明目标串T和模式串W，声明数组M来储存W的内部匹配信息；
     * 2. 从T的i位开始与W进行匹配，最开始i=0。如果W的第k位与T不匹配，那么i+=M[k];然后继续从T的i位开始与W进行匹配，直到T与W匹配。
     * 3. 步骤2的i+=M[k]是KMP算法的核心，所以放到最后来说明：W的第k位与T不匹配，如果使用蛮力法，就会把T右移1位，也就是i++，然后继续匹配，但是这样做效率就低了。W的第k位与T不匹配，意味着W的前k-1位与T是匹配，如果把W的前k-1位右移x位后，W与W的前k-1位仍不匹配，就相当于把T右移x位后，T与W仍不匹配。
     * 4. 现在我们就要找到这个x的最大值，即T最多右移多少位，我们用M[k]表示W的第k位与T不匹配时，T最多右移的位数。当k=0时，肯定要把T右移一位，所以M[0]=1;当k=1时,，由于M[k]不能大于k，所以M[1]=1；当k>1时，声明m与j，令m=j=0，然后进行比较：
     * 1. 如果W[m]！=W[j],说明W开始与T不匹配,T可以右移一位，所以令j=0,m++，M[m]=M[m-1]+1，然后继续比较；
     * 2. 如果W[m]=W[j],说明T右移1位后，T的首位等于W的首位，所以需要比较下一位，令j++,m++，M[m]=M[m-1] ，然后继续比较；
     * 3. 如果出现W[m]=W[j]后，又出现W[m]!=W[j],那么说明W的前M[m-1]位与T不匹配，所以M[m]=M[m-1]+1,再令j=0,m++，然后继续比较；
     * 4. 当m=W的长度时，比较完毕。
     *
     * @param T 目标串
     * @param W 模式串
     * @return 模式串在目标中匹配的起点
     */
    public static int KMP(String T, String W) {
        if (T == null || W == null) {
            return -1;
        }
        //模式串W在目标串T中匹配的起点
        int index = -1;

        //用数组M来存储W的内部匹配信息,M[i]表示
        int[] M = new int[W.length()];
        M[0] = M[1] = 1;
        for (int i = 2, j = 0; i < M.length; i++) {
            if (W.charAt(i) != W.charAt(j)) {
                j = 0;
                M[i] = M[i - 1] + 1;
            } else {
                j++;
                M[i] = M[i - 1];
            }
            ToolShow.log(i + ":" + M[i]);
        }
        //循环比较
        for (int i = 0; i < T.length(); ) {
            //从第i位开始匹配
            for (int j = i, k = 0; j < T.length(); j++) {
                if (T.charAt(j) == W.charAt(k)) {
                    if (k < M.length - 1) {
                        k++;
                    } else {
                        index = i;
                        return index;
                    }
                } else {
                    i += M[k];
                    ToolShow.log("k:" + k + "  i: " + i);
                    break;
                }
            }
        }
        return index;
    }

    /**
     * Floyd算法又称为插点法，是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法，与Dijkstra算法类似。Dijkstra算法只能得到最短路径，Floyd算法还能得到路线。
     * Floyd算法是利用中间点更新两点之间的最短路径，当所有的中间点比较完，就得到了图的最短路径。
     * Floyd算法时间复杂度很大，适合一次运算长时间使用的情景，比如公交系统找最短路径。
     * <p>
     * 首先，获取最短路径：
     * 1. 设有加权图G(V,E),V为顶点集合，E为G的邻接矩阵。声明一个二维数组path，来记录路线，最初时path(j,k) =k，相当于V[j]可以直接到达V[k]。
     * 2. 第一次把V[0]当作中间点，比较E(j,k)与E(j,0)+E(0,k)大小，如果E(j,k)>E(j,0)+E(0,k),则path(j,k) = 0，E(j,k)=E(j,0)+E(0,k)，即V[j]、V[0]、V[k]三个点中，V[j]与V[k]的最短距离为E(j,k)，相当于V[j]与V[k]直接相连，然后E(j,k)为他们的路径。
     * 3. 第二次把V[1]当作中间点，比较E(j,k)与E(j,1)+E(1,k)大小，如果E(j,k)>E(j,1)+E(1,k),则path(j,k) = 1，E(j,k)=E(j,1)+E(1,k)，即V[j]、V[0]、V[1]、V[k]四个点中，V[j]与V[k]的最短距离为E(j,k)。
     * 4. 以此类推，把所有的中间点更新完，就得到了G的最短路径。
     * <p>
     * 然后，通过回推，得G的最短路径对应的路线图：
     * - 如果path(j,k) =k，说明V[j]可以直接到达V[k]；
     * - 如果e=path(j,k),而e！=k，说明经过了中间点V[e];
     * - 然后分别判断path(j,e)与path(e,k),知道找到直达路线，然后把所有中间路线连接起来，即为V[j]与V[k]的最短路线。
     *
     * @param matrix 最短路径矩阵
     * @return 最终路线结果
     */
    public static List<String> floyd(int[][] matrix) {
        if (Tool.isEmpty(matrix)) {
            return null;
        }
        //最短路线矩阵：不包含最终路线
        int[][] path = new int[matrix.length][matrix.length];
        //初始化path
        for (int i = 0; i < matrix.length; i++) {
            int[] mm = matrix[i];
            for (int j = 0; j < mm.length; j++) {
                path[i][j] = j;
            }
        }

        //以i为中间点，更新最短路径
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix.length; j++) {
                for (int k = 0; k < matrix.length; k++) {
                    if (matrix[j][k] > matrix[j][i] + matrix[i][k]) {
                        //保存最短路径
                        matrix[j][k] = matrix[j][i] + matrix[i][k];
                        //保存最短路线
                        path[j][k] = i;
                    }
                }
            }
        }

        //最终路线结果
        List<String> pathList = getPathFloyd(matrix, path);
        return pathList;
    }

    /**
     * 根据最最短路径矩阵和最短路线矩阵回推最终路线
     *
     * @param matrix 最短路径矩阵
     * @param path   最短路线矩阵：不包含最终路线
     * @return 最终路线结果
     */
    public static List<String> getPathFloyd(int[][] matrix, int[][] path) {
        //最终路线结果
        List<String> pathList = new ArrayList<>();
        for (int i = 0; i < path.length; i++) {
            for (int j = 0; j < path.length; j++) {
                //从i到j的最短路径
                int m = matrix[i][j];
                String s = "从" + i + "到 " + j + "： 路径为 " + m + ",路线为 " + i;
                //从i到j的最短路线
                s += getPathFloyd(path, i, j) + "\n";
                pathList.add(s);
            }
        }
        return pathList;
    }

    /**
     * 回推某个点的路径
     *
     * @param path 最短路线矩阵：不包含最终路线
     * @param from 起点
     * @param to   终点
     * @return 最终路线
     */
    public static String getPathFloyd(int[][] path, int from, int to) {
        if (path == null || path.length == 0) {
            return null;
        }
        // from到to的中间点
        int m = path[from][to];
        String s = "";
        //如果直接到达
        if (m == to) {
            s += " -> " + to;
            //如果通过中间点到达
        } else {
            s += getPathFloyd(path, from, m);
            s += getPathFloyd(path, m, to);
        }
        return s;
    }
}
